---
title: Run E2E tests on EAS Build
sidebar_title: Run E2E tests
description: Learn how to set up and run E2E tests on EAS Build with popular libraries such as Detox.
---

import { GithubIcon } from '@expo/styleguide-icons/custom/GithubIcon';

import { BoxLink } from '~/ui/components/BoxLink';
import { Collapsible } from '~/ui/components/Collapsible';
import { ContentSpotlight } from '~/ui/components/ContentSpotlight';
import { Terminal, DiffBlock } from '~/ui/components/Snippet';
import { Step } from '~/ui/components/Step';

> **warning** **Deprecated:** This is an old and archived version of our EAS Build E2E tests guide. [See the latest version of the guide here](/build-reference/e2e-tests/).

With EAS Build, you can build a workflow for running E2E tests for your application. In this guide, you will learn how to use one of the most popular libraries ([Detox](https://wix.github.io/Detox)) to do that.

This guide explains how to run E2E tests with Detox in a bare workflow project. You can use [`@config-plugins/detox`](https://github.com/expo/config-plugins/tree/main/packages/detox) for a managed project, but you may need to adjust some of the instructions in this guide to do so.

## Running iOS tests

<Step label="1">
### Initialize a new bare workflow project

Let's start by initializing a new Expo project, installing and configuring `@config-plugins/detox`, and running `npx expo prebuild` to generate the native projects.

Start with the following commands:

<Terminal
  cmd={[
    '# Initialize a new project',
    '$ npx create-expo-app eas-tests-example',
    '# cd into the project directory',
    '$ cd eas-tests-example',
    '# Install @config-plugins/detox',
    '$ npm install --save-dev @config-plugins/detox',
  ]}
/>

Now, open **app.json** and add the `@config-plugins/detox` plugin to your `plugins` list (this must be done before prebuilding). This will automatically configure the Android native code to support Detox.

```json app.json
{
  "expo": {
    // ...
    "plugins": ["@config-plugins/detox"]
  }
}
```

Run prebuild to generate the native projects:

<Terminal cmd={['$ npx expo prebuild']} />

</Step>

<Step label="2">
### Make home screen interactive

The first step to writing E2E tests is to have something to test &mdash; have an empty app, so let's make our app interactive. You can add a button and display some new text when it's pressed. Later, you will write a test that simulates tapping the button and verifies if the text is displayed as expected.

<div style={{ display: 'flex', justifyContent: 'center' }}>
  <ContentSpotlight
    src="/static/images/eas-build/archive/tests/01-click-me.png"
    className="max-w-[400px]"
  />
  <ContentSpotlight
    src="/static/images/eas-build/archive/tests/02-hi.png"
    className="max-w-[400px]"
  />
</div>

<Collapsible summary="See the source code">

```jsx App.js
import { StatusBar } from 'expo-status-bar';
import { useState } from 'react';
import { Pressable, StyleSheet, Text, View } from 'react-native';

export default function App() {
  const [clicked, setClicked] = useState(false);

  return (
    <View style={styles.container}>
      {!clicked && (
        <Pressable testID="click-me-button" style={styles.button} onPress={() => setClicked(true)}>
          <Text style={styles.text}>Click me</Text>
        </Pressable>
      )}
      {clicked && <Text style={styles.hi}>Hi!</Text>}
      <StatusBar style="auto" />
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#fff',
    alignItems: 'center',
    justifyContent: 'center',
  },
  hi: {
    fontSize: 30,
    color: '#4630EB',
  },
  button: {
    alignItems: 'center',
    justifyContent: 'center',
    paddingVertical: 12,
    paddingHorizontal: 32,
    borderRadius: 4,
    elevation: 3,
    backgroundColor: '#4630EB',
  },
  text: {
    fontSize: 16,
    lineHeight: 21,
    fontWeight: 'bold',
    letterSpacing: 0.25,
    color: 'white',
  },
});
```

</Collapsible>

</Step>

<Step label="3">

### Set up Detox

#### Install dependencies

Let's add two development dependencies to the project - `jest` and `detox`. `jest` (or `mocha`) is required because `detox` does not have its own test-runner.

<Terminal
  cmd={[
    '# Install jest and detox',
    '$ npm install --save-dev jest detox',
    '# Create Detox configuration files',
    '$ npx detox init -r jest',
  ]}
/>

> See the [Getting started](https://wix.github.io/Detox/docs/introduction/getting-started) and [Project setup](https://wix.github.io/Detox/docs/introduction/project-setup) guides in official Detox documentation to learn about any potential updates to this process.

#### Configure Detox

Detox requires you to specify both the build command and the path to the binary produced by it. Technically, the build command is not necessary when running tests on EAS Build, but allows you to run tests locally (for example, using `npx detox build --configuration ios.release`).

Update **.detoxrc.js** with the following configuration with:

```js .detoxrc.js
/** @type {Detox.DetoxConfig} */
module.exports = {
  logger: {
    level: process.env.CI ? 'debug' : undefined,
  },
  testRunner: {
    $0: 'jest',
    args: {
      config: 'e2e/jest.config.js',
      _: ['e2e'],
    },
  },
  artifacts: {
    plugins: {
      log: process.env.CI ? 'failing' : undefined,
      /* @info In Detox 20, this plugin setting will take screenshots if tests fail */
      screenshot: 'failing',
      /* @end */
    },
  },
  apps: {
    'ios.release': {
      type: 'ios.app',
      /* @info This is project-specific, replace eastestsexample with correct value */
      build:
        'xcodebuild -workspace ios/eastestsexample.xcworkspace -scheme eastestsexample -configuration Release -sdk iphonesimulator -arch x86_64 -derivedDataPath ios/build',
      binaryPath: 'ios/build/Build/Products/Release-iphonesimulator/eastestsexample.app',
      /* @end */
    },
    'android.release': {
      type: 'android.apk',
      build:
        'cd android && ./gradlew :app:assembleRelease :app:assembleAndroidTest -DtestBuildType=release && cd ..',
      binaryPath: 'android/app/build/outputs/apk/release/app-release.apk',
    },
  },
  devices: {
    simulator: {
      type: 'ios.simulator',
      device: {
        type: 'iPhone 14',
      },
    },
    emulator: {
      type: 'android.emulator',
      device: {
        avdName: 'pixel_4',
      },
    },
  },
  configurations: {
    'ios.release': {
      device: 'simulator',
      app: 'ios.release',
    },
    'android.release': {
      device: 'emulator',
      app: 'android.release',
    },
  },
};
```

</Step>

<Step label="4">
### Write E2E tests

Next, add your first E2E tests. Delete the auto-generated **e2e/starter.test.js** and create our own **e2e/homeScreen.test.js** with the following snippet:

```js e2e/homeScreen.test.js
describe('Home screen', () => {
  beforeAll(async () => {
    await device.launchApp();
  });

  beforeEach(async () => {
    await device.reloadReactNative();
  });

  it('"Click me" button should be visible', async () => {
    await expect(element(by.id('click-me-button'))).toBeVisible();
  });

  it('shows "Hi!" after tapping "Click me"', async () => {
    await element(by.id('click-me-button')).tap();
    await expect(element(by.text('Hi!'))).toBeVisible();
  });
});
```

There are two tests in the suite:

- One that checks whether the "Click me" button is visible on the home screen.
- Another that verifies that tapping the button triggers displaying "Hi!".

Both tests assume the button has the `testID` set to `click-me-button`. See the [source code](#see-the-source-code) for details.

</Step>

<Step label="5">
### Configure EAS Build

After configuring Detox and writing the initial E2E test, the next step is to configure EAS Build and execute the tests in the cloud.

#### Create eas.json

The following command creates [**eas.json**](/build/eas-json/) in the project's root directory:

<Terminal cmd={['$ eas build:configure']} />

#### Configure EAS Build

There are a few more steps to configure EAS Build for running E2E tests as part of the build:

- Android test:
  - Tests are run in the Android Emulator. You will define a build profile that builds your app for the emulator (produces a **.apk** file).
  - Install the emulator and all its system dependencies.
- iOS test:
  - Tests are run in the iOS Simulator. You will define a build profile that builds your app for the simulator.
  - Install the [`applesimutils`](https://github.com/wix/AppleSimulatorUtils) command line util.
- Configure EAS Build to run Detox tests after successfully building the app.

Edit **eas.json** and add the `test` build profile:

```json eas.json
{
  "build": {
    "test": {
      "android": {
        "gradleCommand": ":app:assembleRelease :app:assembleAndroidTest -DtestBuildType=release",
        "withoutCredentials": true
      },
      "ios": {
        "simulator": true
      }
    }
  }
}
```

Create **eas-hooks/eas-build-pre-install.sh** to install the necessary tools and dependencies for the given platform:

```sh eas-hooks/eas-build-pre-install.sh
#!/usr/bin/env bash

set -eox pipefail

if [[ "$EAS_BUILD_RUNNER" == "eas-build" && "$EAS_BUILD_PROFILE" == "test"* ]]; then
  if [[ "$EAS_BUILD_PLATFORM" == "android" ]]; then
    sudo apt-get --quiet update --yes

    # Install emulator & video bridge dependencies
    # Source: https://github.com/react-native-community/docker-android/blob/master/Dockerfile
    sudo apt-get --quiet install --yes \
      libc6 \
      libdbus-1-3 \
      libfontconfig1 \
      libgcc1 \
      libpulse0 \
      libtinfo5 \
      libx11-6 \
      libxcb1 \
      libxdamage1 \
      libnss3 \
      libxcomposite1 \
      libxcursor1 \
      libxi6 \
      libxext6 \
      libxfixes3 \
      zlib1g \
      libgl1 \
      pulseaudio \
      socat

    # Emulator must be API 31 -- API 32 and 33 fail due to https://github.com/wix/Detox/issues/3762
    sdkmanager --install "system-images;android-31;google_apis;x86_64"
    avdmanager --verbose create avd --force --name "pixel_4" --device "pixel_4" --package "system-images;android-31;google_apis;x86_64"
  else
    brew tap wix/brew
    brew install applesimutils
  fi
fi

```

Next, create **eas-hooks/eas-build-on-success.sh** and add the code snippet as shown below. The script runs different commands for Android and iOS.

- For Android, you have to manually start the emulator before running the tests as `detox` occasionally encounters issues initiating the emulator independently, which can cause a hang during the first test of your suite. After completing the `detox test`, there is a command that kills the previously started emulator.
- For iOS, the only command is `detox test`.

```sh eas-hooks/eas-build-on-success.sh
#!/usr/bin/env bash

function cleanup()
{
  echo 'Cleaning up...'
  if [[ "$EAS_BUILD_PLATFORM" == "android" ]]; then
    # Kill emulator
    adb emu kill &
  fi
}

if [[ "$EAS_BUILD_PROFILE" != "test" ]]; then
  exit
fi

# Fail if anything errors
set -eox pipefail
# If this script exits, trap it first and clean up the emulator
trap cleanup EXIT

ANDROID_EMULATOR=pixel_4

if [[ "$EAS_BUILD_PLATFORM" == "android" ]]; then
  # Start emulator
  $ANDROID_SDK_ROOT/emulator/emulator @$ANDROID_EMULATOR -no-audio -no-boot-anim -no-window -use-system-libs 2>&1 >/dev/null &

  # Wait for emulator
  max_retry=10
  counter=0
  until adb shell getprop sys.boot_completed; do
    sleep 10
    [[ counter -eq $max_retry ]] && echo "Failed to start the emulator!" && exit 1
    counter=$((counter + 1))
  done

  # Execute Android tests
  if [[ "$EAS_BUILD_PROFILE" == "test" ]]; then
    detox test --configuration android.release
  fi
else
  # Execute iOS tests
  if [[  "$EAS_BUILD_PROFILE" == "test" ]]; then
    detox test --configuration ios.release
  fi
fi
```

Update **package.json** to use [EAS Build hooks](/build-reference/npm-hooks/) to run the above scripts on EAS Build:

```json package.json
{
  "scripts": {
    "eas-build-pre-install": "./eas-hooks/eas-build-pre-install.sh",
    "eas-build-on-success": "./eas-hooks/eas-build-on-success.sh"
  }
}
```

> Don't forget to add executable permissions to **eas-build-pre-install.sh** and **eas-build-on-success.sh**. Run `chmod +x eas-hooks/*.sh`.

</Step>

<Step label="6">

### Run tests on EAS Build

Running the tests on EAS Build is like running a regular build:

<Terminal cmd={['$ eas build -p all -e test']} />

If you have set up everything correctly you should see the successful test result in the build logs:

<ContentSpotlight
  src="/static/images/eas-build/archive/tests/03-logs.png"
  className="max-w-[720px]"
/>

</Step>

<Step label="7">
### Upload screenshots of failed test cases

> This step is optional but highly recommended.

When an E2E test case fails, it can be helpful to see the screenshot of the application state. EAS Build makes it easy to upload any arbitrary build artifacts using [`buildArtifactPaths`](/eas/json/#buildartifactpaths) in **eas.json**.

#### Take screenshots for failed tests

Detox supports taking in-test screenshots of the device. The [**.detoxrc.js** sample](/build-reference/e2e-tests/#configure-detox) above includes a configuration for Detox to take screenshots of failed tests.

#### Configure EAS Build for screenshots upload

Edit **eas.json** and add `buildArtifactPaths` to the `test` build profile:

```json eas.json
{
  "build": {
    "test": {
      "android": {
        "gradleCommand": ":app:assembleRelease :app:assembleAndroidTest -DtestBuildType=release",
        "withoutCredentials": true
      },
      "ios": {
        "simulator": true
      },
      /* @info */
      "buildArtifactPaths": ["artifacts/**/*.png"]
      /* @end */
    }
  }
}
```

In contrast to `applicationArchivePath`, the build artifacts defined at `buildArtifactPaths` will be uploaded even if the build fails. All **.png** files from the **artifacts** directory will be packed into a tarball and uploaded to AWS S3. You can download them later from the build details page.

If you run E2E tests locally, remember to add **artifacts** to **.gitignore**:

```sh .gitignore
artifacts/
```

#### Break a test and run a build

To test the new configuration, let's break a test and see that EAS Build uploads the screenshots.

Edit **e2e/homeScreen.test.js** and make the following change:

<DiffBlock source="/static/diffs/e2e-tests-homescreen.diff" />

Run an iOS build with the following command and wait for it to finish:

<Terminal cmd={['$ eas build -p ios -e test']} />

After going to the build details page you should see that the build failed. Use the **"Download artifacts"** button to download and examine the screenshot:

<ContentSpotlight
  src="/static/images/eas-build/archive/tests/04-artifacts.png"
  className="max-w-[720px]"
/>

</Step>

## Repository

<BoxLink
  title="Example repository"
  description="Complete example from this guide is available at GitHub."
  href="https://github.com/expo/eas-tests-example"
  Icon={GithubIcon}
/>

## Alternative approaches

### Using development builds to speed up test run time

The above guide explains how to run E2E tests against a release build of your project, which requires executing a fully native build before each test run. Re-building the native app each time you run E2E tests may not be desirable if only the project JavaScript or assets have changed. However, this is necessary for release builds because the app JavaScript bundle is embedded into the binary.

Instead, use [development builds](/develop/development-builds/introduction/) to load from a local development server or from [published updates](/eas-update/introduction/) to save time and CI resources. This can be done by having your E2E test runner invoke the app with a URL that points to a specific update bundle URL, as described in the [development builds deep linking URLs guide](/develop/development-builds/development-workflows/#deep-linking-urls).

Development builds typically display an onboarding welcome screen when an app is launched for the first time, which intends to provide context about the `expo-dev-client` UI for developers. However, it can interfere with your E2E tests (which expect to interact with your app and not an onboarding screen). To skip the onboarding screen in a test environment, the query parameter `disableOnboarding=1` can be appended to the project URL (an EAS Update URL or a local development server URL).

An example of such a Detox test is shown below. Full example code is available on the [`eas-tests-example`](https://github.com/expo/eas-tests-example) repository.

<Collapsible summary="e2e/homeScreen.test.js">

```js e2e/homeScreen.test.js
/* @info New line */
const { openApp } = require('./utils/openApp');
/* @end */

describe('Home screen', () => {
  beforeEach(async () => {
    /* @info New line */ await openApp(); /* @end */
  });

  it('"Click me" button should be visible', async () => {
    await expect(element(by.id('click-me-button'))).toBeVisible();
  });

  it('shows "Hi!" after tapping "Click me"', async () => {
    await element(by.id('click-me-button')).tap();
    await expect(element(by.text('Hi!'))).toBeVisible();
  });
});
```

</Collapsible>

<Collapsible summary="e2e/utils/openApp.js (new file)">

```js e2e/utils/openApp.js
const appConfig = require('../../app.json');
const { resolveConfig } = require('detox/internals');

const platform = device.getPlatform();

module.exports.openApp = async function openApp() {
  const config = await resolveConfig();
  if (config.configurationName.split('.')[1] === 'debug') {
    return await openAppForDebugBuild(platform);
  } else {
    return await device.launchApp({
      newInstance: true,
    });
  }
};

async function openAppForDebugBuild(platform) {
  const deepLinkUrl = process.env.EXPO_USE_UPDATES
    ? // Testing latest published EAS update for the test_debug channel
      getDeepLinkUrl(getLatestUpdateUrl())
    : // Local testing with packager
      getDeepLinkUrl(getDevLauncherPackagerUrl(platform));

  if (platform === 'ios') {
    await device.launchApp({
      newInstance: true,
    });
    sleep(3000);
    await device.openURL({
      url: deepLinkUrl,
    });
  } else {
    await device.launchApp({
      newInstance: true,
      url: deepLinkUrl,
    });
  }

  await sleep(3000);
}

const getDeepLinkUrl = url =>
  `eastestsexample://expo-development-client/?url=${encodeURIComponent(url)}`;

const getDevLauncherPackagerUrl = platform =>
  `http://localhost:8081/index.bundle?platform=${platform}&dev=true&minify=false&disableOnboarding=1`;

const getLatestUpdateUrl = () =>
  `https://u.expo.dev/${getAppId()}?channel-name=test_debug&disableOnboarding=1`;

const getAppId = () => appConfig?.expo?.extra?.eas?.projectId ?? '';

const sleep = t => new Promise(res => setTimeout(res, t));
```

</Collapsible>

<Collapsible summary=".detoxrc.js">

```js .detoxrc.js
/** @type {Detox.DetoxConfig} */
module.exports = {
  logger: {
    level: process.env.CI ? 'debug' : undefined,
  },
  testRunner: {
    $0: 'jest',
    args: {
      config: 'e2e/jest.config.js',
      _: ['e2e'],
    },
  },
  artifacts: {
    plugins: {
      log: process.env.CI ? 'failing' : undefined,
      screenshot: 'failing',
    },
  },
  apps: {
    'ios.debug': {
      type: 'ios.app',
      build:
        'xcodebuild -workspace ios/eastestsexample.xcworkspace -scheme eastestsexample -configuration Debug -sdk iphonesimulator -arch x86_64 -derivedDataPath ios/build',
      binaryPath: 'ios/build/Build/Products/Debug-iphonesimulator/eastestsexample.app',
    },
    'ios.release': {
      type: 'ios.app',
      build:
        'xcodebuild -workspace ios/eastestsexample.xcworkspace -scheme eastestsexample -configuration Release -sdk iphonesimulator -arch x86_64 -derivedDataPath ios/build',
      binaryPath: 'ios/build/Build/Products/Release-iphonesimulator/eastestsexample.app',
    },
    'android.debug': {
      type: 'android.apk',
      build:
        'cd android && ./gradlew :app:assembleDebug :app:assembleAndroidTest -DtestBuildType=debug && cd ..',
      binaryPath: 'android/app/build/outputs/apk/debug/app-debug.apk',
    },
    'android.release': {
      type: 'android.apk',
      build:
        'cd android && ./gradlew :app:assembleRelease :app:assembleAndroidTest -DtestBuildType=release && cd ..',
      binaryPath: 'android/app/build/outputs/apk/release/app-release.apk',
    },
  },
  devices: {
    simulator: {
      type: 'ios.simulator',
      device: {
        type: 'iPhone 14',
      },
    },
    emulator: {
      type: 'android.emulator',
      device: {
        avdName: 'pixel_4',
      },
    },
  },
  configurations: {
    'ios.debug': {
      device: 'simulator',
      app: 'ios.debug',
    },
    'ios.release': {
      device: 'simulator',
      app: 'ios.release',
    },
    'android.debug': {
      device: 'emulator',
      app: 'android.debug',
    },
    'android.release': {
      device: 'emulator',
      app: 'android.release',
    },
  },
};
```

</Collapsible>

<Collapsible summary="eas-hooks/eas-build-on-success.sh">

```sh eas-hooks/eas-build-on-success.sh
#!/usr/bin/env bash

function cleanup()
{
  echo 'Cleaning up...'
  if [[ "$EAS_BUILD_PLATFORM" == "android" ]]; then
    # Kill emulator
    adb emu kill &
  fi
}

if [[ "$EAS_BUILD_PROFILE" != "test"* ]]; then
  exit
fi

# Fail if anything errors
set -eox pipefail
# If this script exits, trap it first and clean up the emulator
trap cleanup EXIT

ANDROID_EMULATOR=pixel_4

if [[ "$EAS_BUILD_PLATFORM" == "android" ]]; then
  # Start emulator
  $ANDROID_SDK_ROOT/emulator/emulator @$ANDROID_EMULATOR -no-audio -no-boot-anim -no-window -use-system-libs 2>&1 >/dev/null &

  # Wait for emulator
  max_retry=10
  counter=0
  until adb shell getprop sys.boot_completed; do
    sleep 10
    [[ counter -eq $max_retry ]] && echo "Failed to start the emulator!" && exit 1
    counter=$((counter + 1))
  done


  # Execute Android tests
  if [[ "$EAS_BUILD_PROFILE" == "test" ]]; then
    detox test --configuration android.release
  fi
  if [[ "$EAS_BUILD_PROFILE" == "test_debug" ]]; then
    detox test --configuration android.debug
  fi
else
  # Execute iOS tests
  if [[  "$EAS_BUILD_PROFILE" == "test" ]]; then
    detox test --configuration ios.release
  fi
  if [[ "$EAS_BUILD_PROFILE" == "test_debug" ]]; then
    detox test --configuration ios.debug
  fi
fi
```

</Collapsible>

<Collapsible summary="eas.json">

```json eas.json
{
  "build": {
    "test": {
      "android": {
        "gradleCommand": ":app:assembleRelease :app:assembleAndroidTest -DtestBuildType=release",
        "withoutCredentials": true
      },
      "ios": {
        "simulator": true
      }
    },
    /* @info New section */ "test_debug": {
      "android": {
        "gradleCommand": ":app:assembleDebug :app:assembleAndroidTest -DtestBuildType=debug",
        "withoutCredentials": true
      },
      "ios": {
        "buildConfiguration": "Debug",
        "simulator": true
      },
      "env": {
        "EXPO_USE_UPDATES": "1"
      },
      "channel": "test_debug"
    } /* @end */
  }
}
```

</Collapsible>
